/**
 * Radio Story Cards - using RFID tags to track physical story cards in an agile/lean environment.
 *
 * See http://www.radiostorycards.com for more information.
 *
 *
 * Copyright (C) 2011, Ted M. Young
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.radiostorycards.comm;

import com.radiostorycards.domain.tags.RfidTag;
import com.radiostorycards.event.TagSwipedEvent;
import gnu.io.*;
import org.bushe.swing.event.EventBus;

import java.io.IOException;
import java.io.InputStream;
import java.util.TooManyListenersException;

/**
 * Reads data from a serial port (hard-coded in a constant below, for now), that notifies registered listeners when
 * new data has come in from the RFID reader. It will use an {@link RfidReaderTagTranslator} that's appropriate for
 * the reader.
 *
 * @author Ted Young <tedyoung@gmail.com> @JitterTed
 */
public class SerialReader implements SerialPortEventListener {
  public static final String RFID_READER_COM_PORT_NAME = "COM25";
  public static final int REPEAT_THRESHOLD_IN_MILLIS = 2000;
  public static final int SERIAL_READ_END_OF_STREAM = -1;
  private InputStream _serialPortInputStream;
  private StringBuffer _inputString = new StringBuffer(32);

  // initialize last event to ensure the absolute first event we see is recognized
  private long _lastEventMillis = System.currentTimeMillis() - REPEAT_THRESHOLD_IN_MILLIS;
  private RfidReaderTagTranslator _translator;

  public SerialReader(InputStream inputStream, RfidReaderTagTranslator translator) {
    _serialPortInputStream = inputStream;
    _translator = translator;
  }

  public static SerialReader createSerialReaderFor(RfidReaderType rfidReaderType) throws
      NoSuchPortException, PortInUseException, IOException, TooManyListenersException, UnsupportedCommOperationException {

    CommPortIdentifier portId = getPortFor(RFID_READER_COM_PORT_NAME);

    SerialPort serialPort = openSerialPort(portId, 2400);

    RfidReaderTagTranslator translator = createTranslatorFor(rfidReaderType);

    return createReaderFor(serialPort, translator);
  }

  private static RfidReaderTagTranslator createTranslatorFor(RfidReaderType rfidReaderType) {
    RfidReaderTagTranslator translator;
    if (rfidReaderType == RfidReaderType.Parallax) {
      translator = new ParallaxTranslator();
    } else if (rfidReaderType == RfidReaderType.RedBee) {
      translator = new RedBeeTranslator();
    } else {
      throw new IllegalArgumentException("Unknown RFID Reader Type: " + rfidReaderType);
    }
    return translator;
  }

  private static CommPortIdentifier getPortFor(String comPortName) throws NoSuchPortException {
    CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier(comPortName);
    System.out.println("Port ID: " + portId.getName() + ", Port Type: " + portId.getPortType());
    return portId;
  }

  private static SerialPort openSerialPort(CommPortIdentifier portId, int portSpeed) throws PortInUseException,
      UnsupportedCommOperationException {
    SerialPort serialPort = (SerialPort) portId.open("SerialReader", 3 * 1000);// wait up to 3 seconds to acquire port
    setupSerialPort(serialPort, portSpeed);
    System.out.println("Serial Port: " + serialPort.getName() + " has been opened at speed of " + portSpeed);
    return serialPort;
  }

  private static SerialReader createReaderFor(SerialPort serialPort, RfidReaderTagTranslator translator) throws IOException,
      TooManyListenersException {
    InputStream inputStream = serialPort.getInputStream();
    SerialReader serialReader = new SerialReader(inputStream, translator);
    serialPort.addEventListener(serialReader);
    return serialReader;
  }

  private static void setupSerialPort(SerialPort serialPort, int portSpeed) throws UnsupportedCommOperationException {
    serialPort.notifyOnDataAvailable(true);
    serialPort.setSerialPortParams(portSpeed,
        SerialPort.DATABITS_8,
        SerialPort.STOPBITS_1,
        SerialPort.PARITY_NONE);
    serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
  }

  public void serialEvent(SerialPortEvent serialPortEvent) {
    if (serialPortEvent.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
      serialDataReceived(_serialPortInputStream);
    }
  }

  public void serialDataReceived(InputStream inputStream) {
    try {
      while (inputStream.available() > 0) {
        char inputByte = (char) inputStream.read();
        if (_translator.isEndOfTag(inputByte)) {
          RfidTag rfidTag = _translator.rfidStringToTag(_inputString.toString());
          notifySubscribersOfTag(rfidTag);
          _inputString = new StringBuffer(32);
        } else if (inputByte == SERIAL_READ_END_OF_STREAM) {
          throw new IllegalStateException(
              "Should not have gotten a -1 byte (end-of-data) read if the input stream has available data.");
        } else {
          _inputString.append(inputByte);
        }
      }
    } catch (IOException e) {
      System.err.println("Input String so far:\"" + _inputString + "\"");
      e.printStackTrace();
    }
  }

  private void notifySubscribersOfTag(final RfidTag rfidTag) {
    if (isNotARepeat()) {
      EventBus.publish(new TagSwipedEvent(rfidTag));
    }
  }

  /**
   * The RFID reader continually sends the Tag's ID as long as it's held near the reader. This method indicates whether
   * this event took place within the threshold of the prior event so that we can tell the difference between the RFID
   * auto-repeat, and the user intentionally swiping the same tag twice.
   *
   * @return true if this is a new tag event
   */
  private boolean isNotARepeat() {
    long currentEventMillis = System.currentTimeMillis();
    long millisElapsed = currentEventMillis - _lastEventMillis;
    if (millisElapsed > REPEAT_THRESHOLD_IN_MILLIS) {
      _lastEventMillis = currentEventMillis;
      return true;
    } else {
      return false;
    }
  }

}
